#
# This file is a part of the open-eBackup project.
# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
# If a copy of the MPL was not distributed with this file, You can obtain one at
# http://mozilla.org/MPL/2.0/.
#
# Copyright (c) [2024] Huawei Technologies Co.,Ltd.
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#

import threading
import time

from common.common_models import ActionResult, LogDetail
from common.const import DBLogLevel, ReportDBLabel
from common.const import ExecuteResultEnum, SubJobStatusEnum, RestoreTypeEnum
from common.logger import Logger
from common.number_const import NumberConst
from db2.backup.util.db2_backup_util import Db2BackupUtil
from db2.comm.const import Db2Const
from db2.comm.error_code import Db2ErrCode
from db2.comm.util.common_util import Db2CommonUtil
from db2.comm.util.dpf_util import DpfUtil
from db2.comm.util.ha_util import HaUtil
from db2.comm.util.job_decorators import job_exception_decorator
from db2.comm.util.param_util import Db2ParamUtil
from db2.comm.util.single_util import SingleUtil
from db2.restore.db2_restore_base import Db2RestoreBase
from db2.restore.single.single_ts_restore_service import SingleRestoreService

LOGGER = Logger().get_logger(filename="db2.log")


class SingleTsRestore(Db2RestoreBase):

    def __init__(self, task_name, pid, job_id, sub_job_id, param_dict):
        super().__init__(task_name, pid, job_id, sub_job_id, param_dict)
        self._os_user = Db2ParamUtil.get_tgt_os_user_when_restore(self.pid)
        self._db_name = Db2ParamUtil.get_db_name_for_ts_restore(self.param_dict)
        self._ts_name = Db2ParamUtil.get_ts_name_for_restore(self.param_dict)
        self._restore_type = Db2ParamUtil.get_restore_type(self.param_dict)
        self.log_detail = LogDetail(logInfo=ReportDBLabel.RESTORE_SUB_START_COPY,
                                    logInfoParam=[self.sub_job_id], logLevel=DBLogLevel.INFO.value)

    @job_exception_decorator(is_restore=True)
    def allow_restore_in_local_node(self):
        tgt_ts_names = Db2ParamUtil.get_ts_name_for_restore(self.param_dict)
        src_ts_names = Db2ParamUtil.get_table_spaces_of_ts_copy(self.param_dict)
        if not tgt_ts_names or not src_ts_names or set(tgt_ts_names).difference(src_ts_names):
            LOGGER.error(f"The tablespaces are inconsistent or do not exist, source "
                         f"tablespaces: {src_ts_names}, target tablespaces: {tgt_ts_names}, job id: {self.job_id}.")
            err_param = SingleUtil.build_table_space_not_exists_message(src_ts_names, tgt_ts_names)
            if not err_param:
                err_param = SingleUtil.build_table_space_not_exists_message(tgt_ts_names, src_ts_names)
                return Db2CommonUtil.build_action_result(code=ExecuteResultEnum.INTERNAL_ERROR.value,
                                                         message="The tablespaces are inconsistent or do not exist",
                                                         body_err=Db2ErrCode.SPACE_NOT_EXISTS, err_params=[err_param])
        # 检查源实例和目标实例运行和维护数据库的操作系统实例是否一致
        src_inst_name = Db2ParamUtil.get_instance_name_for_restore(self.param_dict)
        tgt_inst_name = Db2ParamUtil.get_tgt_instance_name_when_restore_ts(self.param_dict)
        if not tgt_inst_name or not src_inst_name or tgt_inst_name != src_inst_name:
            LOGGER.error(f"The instance name is inconsistent or does not exist, source instance: {src_inst_name}")
            return Db2CommonUtil.build_action_result(code=ExecuteResultEnum.INTERNAL_ERROR.value,
                                                     message="The instance name is inconsistent or does not exist",
                                                     body_err=Db2ErrCode.DB_INSTANCE_NOT_EQUAL,
                                                     err_params=[src_inst_name, tgt_inst_name])
        # 检查数据库是否正常存在于该实例中
        src_db_name = Db2ParamUtil.get_db_name_of_ts_copy(self.param_dict)
        if not self._db_name or not src_inst_name or self._db_name != src_db_name:
            LOGGER.error(f"During restore tablespaces, the database name is inconsistent or does not exist, source "
                         f"database: {src_db_name}, target database: {self._db_name}, job id: {self.job_id}.")
            return Db2CommonUtil.build_action_result(code=ExecuteResultEnum.INTERNAL_ERROR.value,
                                                     message="The database name is inconsistent or does not exist",
                                                     body_err=Db2ErrCode.SPACE_OF_DB_NOT_EQUAL,
                                                     err_params=[src_db_name, self._db_name])
        # 校验表空间所在的数据库是否异常
        pending_ret, pending_info_str = DpfUtil.check_database_pending_status_for_restore(self._os_user, self._db_name)
        # 检查目标数据库状态是否正常
        if pending_ret:
            LOGGER.error("There are pending tasks in the db2 single database .")
            return Db2CommonUtil.build_action_result(
                code=ExecuteResultEnum.INTERNAL_ERROR.value, body_err=Db2ErrCode.DATABASE_EXISTS_PENDING,
                err_params=[pending_info_str], message="There are pending tasks in the db2 single database")

        # 检查表空间状态
        ret, action_result = self.check_tablespace_status()
        if not ret:
            return action_result

        # 检查表状态
        ret, action_result = self.check_table_status()
        if not ret:
            return action_result
        return Db2CommonUtil.build_action_result(code=ExecuteResultEnum.SUCCESS.value)

    @job_exception_decorator(write_progress=True, is_restore=True)
    def exec_restore_pre_job(self):
        LOGGER.info(f"Start executing restore pre job, pid: {self.pid}, job id: {self.job_id}, "
                    f"sub jod id: {self.sub_job_id}")
        self.sub_job_detail.progress = NumberConst.HUNDRED
        self.sub_job_detail.task_status = SubJobStatusEnum.COMPLETED.value
        Db2CommonUtil.proactively_report_progress(pid=self.pid, job_detail=self.sub_job_detail)
        LOGGER.info(f"Execute restore tablespaces pre job finished, pid: {self.pid}, job id: {self.job_id}")
        return Db2CommonUtil.build_action_result(code=ExecuteResultEnum.SUCCESS.value)

    @job_exception_decorator(write_progress=True, is_restore=True)
    def exec_restore(self):
        s_job_name = Db2ParamUtil.get_sub_job_name(self.param_dict)
        LOGGER.info(f"Start executing restore tablespaces job, sub job name: {s_job_name}, pid: {self.pid}, "
                    f"job id: {self.job_id}, sub job id: {self.sub_job_id}.")
        self.sub_job_detail.progress = NumberConst.FIVE
        self.sub_job_detail.task_status = SubJobStatusEnum.RUNNING.value
        self.sub_job_detail.log_detail = [self.log_detail]
        Db2CommonUtil.proactively_report_progress(pid=self.pid, job_detail=self.sub_job_detail)
        # 上报完开始执行子任务后将标签清理,恢复agent框架在备份加了子任务执行开始lable,恢复未加
        self.sub_job_detail.log_detail = []
        progress_thread = threading.Thread(name="progress", target=self.upload_report_progress)
        # 任务意外结束时子线程也结束任务
        progress_thread.daemon = True
        progress_thread.start()

        res_ts_names = Db2ParamUtil.get_restore_table_spaces(self.param_dict)
        if self._restore_type == RestoreTypeEnum.FULL_RESTORE.value:
            SingleRestoreService.handle_full_restore_ts(self._os_user, self._db_name, res_ts_names, self.param_dict)
        elif self._restore_type == RestoreTypeEnum.INCRE_RESTORE.value:
            SingleRestoreService.handle_incr_restore_ts(self._os_user, self._db_name, res_ts_names, self.param_dict)
        elif self._restore_type == RestoreTypeEnum.DIFF_RESTORE.value:
            SingleRestoreService.handle_diff_restore_ts(self._os_user, self._db_name, res_ts_names, self.param_dict)
        else:
            self.report_fail_progress_when_not_restore_type()
            return Db2CommonUtil.build_action_result(code=ExecuteResultEnum.INTERNAL_ERROR.value,
                                                     message="Unsupported backup type",
                                                     body_err=Db2ErrCode.ERROR_COMMON_INVALID_PARAMETER)
        # 恢复后检查恢复表空间状态是否正常
        all_ts_dict = Db2CommonUtil.get_table_space_info_dict_of_db(self._os_user, self._db_name)
        chk_bak_ts_state_ret = list(all_ts_dict.get(i, {}).get("state") == Db2Const.NORMAL_STATE for i in res_ts_names)
        if not all(chk_bak_ts_state_ret):
            self.report_fail_progress_when_not_chk_bak(res_ts_names)
            return Db2CommonUtil.build_action_result(code=ExecuteResultEnum.INTERNAL_ERROR.value,
                                                     message="There are tablespaces whose state is not "
                                                             "normal in the backup table_spaces",
                                                     body_err=Db2ErrCode.SPACE_STATUS_ABNORMAL,
                                                     err_params=[str(res_ts_names)])
        LOGGER.info("The state of the restored tablespaces is normal.")
        self.sub_job_detail.task_status = SubJobStatusEnum.COMPLETED.value
        self.sub_job_detail.progress = NumberConst.HUNDRED
        self.log_detail.log_info = ReportDBLabel.SUB_JOB_SUCCESS
        self.log_detail.log_level = DBLogLevel.INFO.value
        self.sub_job_detail.log_detail = [self.log_detail]
        Db2CommonUtil.proactively_report_progress(pid=self.pid, job_detail=self.sub_job_detail)
        LOGGER.info(f"Execute restore tablespaces job finished, pid: {self.pid}, job id: {self.job_id}, "
                    f"sub job id: {self.sub_job_id}.")
        return Db2CommonUtil.build_action_result(code=ExecuteResultEnum.SUCCESS.value)

    def report_fail_progress_when_not_chk_bak(self, res_ts_names):
        LOGGER.error(f"There is an abnormal state in the restored tablespaces after restoring, "
                     f"database name: {self._db_name}, restored tablespaces: {res_ts_names}.")
        self.sub_job_detail.task_status = SubJobStatusEnum.FAILED.value
        self.sub_job_detail.progress = NumberConst.NINETY
        self.log_detail.log_info = ReportDBLabel.SUB_JOB_FALIED
        self.log_detail.log_level = DBLogLevel.ERROR.value
        self.sub_job_detail.log_detail = [self.log_detail]
        Db2CommonUtil.proactively_report_progress(pid=self.pid, job_detail=self.sub_job_detail)

    def report_fail_progress_when_not_restore_type(self):
        LOGGER.error(f"Unsupported restore type: {self._restore_type}.")
        self.sub_job_detail.task_status = SubJobStatusEnum.FAILED.value
        self.log_detail.log_info = ReportDBLabel.SUB_JOB_FALIED
        self.log_detail.log_level = DBLogLevel.ERROR.value
        self.sub_job_detail.log_detail = [self.log_detail]
        Db2CommonUtil.proactively_report_progress(pid=self.pid, job_detail=self.sub_job_detail)

    @job_exception_decorator(write_progress=True, is_restore=True)
    def exec_restore_post_job(self):
        LOGGER.info(f"Start executing restore tablespaces post job, pid: {self.pid}, job id: {self.job_id}, "
                    f"sub jod id: {self.sub_job_id}.")
        self.sub_job_detail.progress = NumberConst.FIVE
        Db2CommonUtil.proactively_report_progress(pid=self.pid, job_detail=self.sub_job_detail)
        HaUtil.handle_unmount_path_for_san(self.param_dict)
        # 恢复后置任务
        if self._restore_type in (RestoreTypeEnum.INCRE_RESTORE.value, RestoreTypeEnum.DIFF_RESTORE.value):
            copies = Db2ParamUtil.parse_copies(self.param_dict)
            full_copy_path = Db2ParamUtil.get_full_copy_path_for_incr_and_diff_restore(copies, self.job_id)
            Db2CommonUtil.clear_soft_link_of_dir(full_copy_path)
            LOGGER.info(f"Remove soft link of tablespaces full copy success, restore type: {self._restore_type}.")
        self.sub_job_detail.progress = NumberConst.HUNDRED
        self.sub_job_detail.task_status = SubJobStatusEnum.COMPLETED.value
        Db2CommonUtil.proactively_report_progress(pid=self.pid, job_detail=self.sub_job_detail)
        LOGGER.info(f"Execute restore tablespaces post job finished, pid: {self.pid}, job id: {self.job_id}, "
                    f"sub job id: {self.sub_job_id}.")
        return Db2CommonUtil.build_action_result(code=ExecuteResultEnum.SUCCESS.value)

    def report_progress(self):
        cache_path = Db2ParamUtil.get_cache_mount_path_for_restore(self.param_dict)
        if self.sub_job_id:
            Db2BackupUtil.report_progress_utl(self.pid, self.sub_job_id, cache_path)
        else:
            Db2BackupUtil.report_progress_utl(self.pid, self.job_id, cache_path)

    def output_action_result(self, action_ret: ActionResult):
        super(SingleTsRestore, self).output_action_result(action_ret)

    def check_tablespace_status(self):
        db_name = Db2ParamUtil.get_db_name_of_ts_copy(self.param_dict)
        tgt_ts_names = Db2ParamUtil.get_ts_name_for_restore(self.param_dict)
        tablespace_info = Db2ParamUtil.get_ts_info_for_restore(self.param_dict)
        tablespace_set = Db2CommonUtil.check_tablespace_status(self._os_user, db_name, tgt_ts_names, tablespace_info)
        if tablespace_set:
            ts_names = ",".join(dict(tablespace_set).keys())
            LOGGER.error("The table spaces of the database does not exists.")
            return False, Db2CommonUtil.build_action_result(
                code=ExecuteResultEnum.INTERNAL_ERROR.value,
                message="The table spaces of the database does not exists",
                body_err=Db2ErrCode.SPACE_NOT_EXISTS, err_params=[ts_names])
        return True, Db2CommonUtil.build_action_result(code=ExecuteResultEnum.SUCCESS.value)

    def check_table_status(self):
        db_name = Db2ParamUtil.get_db_name_of_ts_copy(self.param_dict)
        tgt_ts_names = Db2ParamUtil.get_ts_name_for_restore(self.param_dict)
        table_info = Db2ParamUtil.get_table_info_for_restore(self.param_dict)
        table_set = Db2CommonUtil.check_table_status(self._os_user, db_name, tgt_ts_names, table_info)
        if table_set:
            LOGGER.error("Table is delete in backup.")
            table_names = list(dict(table_set).keys())
            table_spaces_str = self.param_dict.get("job", {}).get("targetObject", {}).\
                get("extendInfo", {}).get("table", "")
            return False, Db2CommonUtil.build_action_result(code=ExecuteResultEnum.INTERNAL_ERROR.value,
                                                            message="Table is deleted!",
                                                            body_err=Db2ErrCode.TABLESPACES_DROP_TABLE,
                                                            err_params=[table_names[0], table_spaces_str,
                                                                        table_names[0]])
        return True, Db2CommonUtil.build_action_result(code=ExecuteResultEnum.SUCCESS.value)

    def upload_report_progress(self):
        # 只关心运行中的任务
        while True:
            if self.sub_job_detail.task_status == SubJobStatusEnum.RUNNING.value:
                progress = Db2CommonUtil.get_now_backup_process(self._os_user, self._db_name)
                # 防止前面任务未写进度的情况。
                if self.sub_job_detail.progress and self.sub_job_detail.progress < progress:
                    self.sub_job_detail.progress = progress
                Db2CommonUtil.proactively_report_progress(pid=self.pid, job_detail=self.sub_job_detail)
            else:
                LOGGER.debug(f"Stop report progress thread. current job status is : {self.sub_job_detail.dict()}")
                break
            LOGGER.debug(f"Report single db2 running progress : {self.sub_job_detail.dict()}")
            time.sleep(NumberConst.TEN)
